/*
 * MIT License
 *
 * Copyright (c) 2024 org.zzz (https://gitee.com/frostforest)
 *
 * Permission is hereby granted, free of charge, to any person obtaining a copy
 * of this software and associated documentation files (the "Software"), to deal
 * in the Software without restriction, including without limitation the rights
 * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
 * copies of the Software, and to permit persons to whom the Software is
 * furnished to do so, subject to the following conditions:
 *
 * The above copyright notice and this permission notice shall be included in all
 * copies or substantial portions of the Software.
 *
 * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
 * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
 * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
 * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
 * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
 * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
 * SOFTWARE.
 */

package org.zzz.pcdc;

import java.util.Arrays;
import java.util.Collections;
import java.util.Objects;
import java.util.Set;
import java.util.stream.Collectors;
import java.util.stream.Stream;

/**
 * MySQL表配置类，用于定义变更数据捕获(CDC)的表结构信息。
 * <p>
 * 此类存储CDC操作所需的表名、列名和其他配置信息，包括：
 * <ul>
 *   <li>CDC目标表名</li>
 *   <li>更新时间戳列名</li>
 *   <li>ID列名</li>
 *   <li>需要监控变更的列</li>
 * </ul>
 * <p>
 * 表名和列名必须符合以下规则：
 * <ul>
 *   <li>以字母开头</li>
 *   <li>只包含字母、数字和下划线</li>
 *   <li>不是MySQL保留关键字</li>
 * </ul>
 * <p>
 * 示例用法：
 * <pre>{@code
 * // 使用默认配置创建表配置
 * PcdcTableConfig config = new PcdcTableConfig("users");
 * 
 * // 自定义时间戳和ID列
 * PcdcTableConfig config = new PcdcTableConfig("users", "modified_at", "user_id");
 * 
 * // 指定要监控的特定列
 * PcdcTableConfig config = new PcdcTableConfig("users", 
 *                                             new String[]{"name", "email", "status"});
 * }</pre>
 * 
 * @author org.zzz
 * @since 1.0
 */
public class PcdcTableConfig {
    /**
     * 表示选择所有列的常量。
     * <p>
     * 当未指定特定列时，此常量用于在SQL查询中选择表的所有列。
     * 
     * @since 1.0
     */
    public static final String ALL_COLUMNS = " * ";
    
    /**
     * MySQL保留关键字集合。
     * <p>
     * 此集合包含可能导致SQL注入风险的MySQL关键字，用于验证表名和列名的安全性。
     * 
     * @since 1.0
     */
    private static final Set<String> RISKY_MYSQL_KEYWORDS = Collections.unmodifiableSet(
            Stream.of(
                            "ALTER",    // 修改表结构
                            "BACKUP",   // 备份操作
                            "BINLOG",   // 二进制日志操作
                            "COMMIT",   // 提交事务
                            "CREATE",   // 创建对象
                            "DELETE",   // 删除数据
                            "DROP",     // 删除对象
                            "EXECUTE",  // 执行动态SQL
                            "FLUSH",    // 刷新缓存
                            "GRANT",    // 授权
                            "INSTALL",  // 安装插件
                            "KILL",     // 终止会话
                            "LOAD",     // 加载数据
                            "LOCK",     // 锁表
                            "OPTIMIZE", // 优化表
                            "PASSWORD", // 密码操作
                            "PRIVILEGES", // 权限查看
                            "PURGE",    // 彻底删除
                            "RENAME",   // 重命名对象
                            "REPAIR",   // 修复表
                            "REPLACE",  // 替换数据
                            "RESET",    // 重置状态
                            "RESTORE",  // 恢复数据
                            "REVOKE",   // 撤销权限
                            "ROLLBACK", // 回滚事务
                            "SHUTDOWN", // 关闭数据库
                            "START",    // 启动操作
                            "STOP",     // 停止操作
                            "TRUNCATE", // 清空表
                            "UNINSTALL", // 卸载插件
                            "UPDATE",   // 更新数据
                            "USE"       // 切换数据库
                    )
                    .collect(Collectors.toSet())
    );
    /**
     * MySQL标识符的正则表达式模式。
     * <p>
     * 此模式用于验证表名和列名是否符合MySQL标识符的命名规则：
     * 以字母开头，后跟字母、数字或下划线。
     * 
     * @since 1.0
     */
    private static final String MYSQL_IDENTIFIERS_PATTERN = "[a-zA-Z][0-9a-zA-Z_]*";

    /**
     * 默认的更新时间戳列名。
     * <p>
     * 当未指定更新时间戳列名时使用此默认值。
     * 
     * @since 1.0
     */
    private static final String DEFAULT_UPDATED_AT_COLUMN_NAME = "updated_at";
    
    /**
     * 默认的ID列名。
     * <p>
     * 当未指定ID列名时使用此默认值。
     * 
     * @since 1.0
     */
    private static final String DEFAULT_ID_COLUMN_NAME = "id";

    /**
     * 要监控变更的MySQL表名。
     */
    private final String cdcTableName;
    
    /**
     * 包含记录最后更新时间戳的列名。
     */
    private final String updatedAtColumnName;
    
    /**
     * 包含记录唯一标识符的列名。
     */
    private final String idColumnName;
    
    /**
     * 需要监控变更的列名列表，以逗号分隔的SQL格式。
     * <p>
     * 如果未指定特定列，则使用{@link #ALL_COLUMNS}表示监控所有列的变更。
     */
    private final String columnsToReportOnChange;

    /**
     * 创建一个新的表配置，使用指定的表名和默认列名。
     * <p>
     * 此构造函数使用默认的更新时间戳列名({@value #DEFAULT_UPDATED_AT_COLUMN_NAME})
     * 和默认的ID列名({@value #DEFAULT_ID_COLUMN_NAME})，并监控表中的所有列。
     *
     * @param cdcTableName 要监控变更的MySQL表名
     * @throws IllegalArgumentException 如果提供的表名无效或是MySQL保留关键字
     * @since 1.0
     */
    public PcdcTableConfig(String cdcTableName) {
        this(cdcTableName,
                DEFAULT_UPDATED_AT_COLUMN_NAME,
                DEFAULT_ID_COLUMN_NAME,
                null);
    }

    /**
     * 创建一个新的表配置，使用指定的表名和要监控的列。
     * <p>
     * 此构造函数使用默认的更新时间戳列名({@value #DEFAULT_UPDATED_AT_COLUMN_NAME})
     * 和默认的ID列名({@value #DEFAULT_ID_COLUMN_NAME})，但允许指定要监控变更的特定列。
     *
     * @param cdcTableName 要监控变更的MySQL表名
     * @param columnsToReportOnChange 要监控变更的列名数组，如果为null或空数组则监控所有列
     * @throws IllegalArgumentException 如果提供的表名或列名无效或是MySQL保留关键字
     * @since 1.0
     */
    public PcdcTableConfig(String cdcTableName,
                           String[] columnsToReportOnChange) {
        this(cdcTableName,
                DEFAULT_UPDATED_AT_COLUMN_NAME,
                DEFAULT_ID_COLUMN_NAME,
                columnsToReportOnChange);
    }

    /**
     * 创建一个新的表配置，使用指定的表名和自定义列名。
     * <p>
     * 此构造函数允许指定CDC操作使用的更新时间戳列和ID列的名称，并监控表中的所有列。
     *
     * @param cdcTableName 要监控变更的MySQL表名
     * @param updatedAtColumnName 包含记录最后更新时间戳的列名
     * @param idColumnName 包含记录唯一标识符的列名
     * @throws IllegalArgumentException 如果提供的表名或列名无效或是MySQL保留关键字
     * @since 1.0
     */
    public PcdcTableConfig(String cdcTableName,
                           String updatedAtColumnName,
                           String idColumnName) {
        this(cdcTableName,
                updatedAtColumnName,
                idColumnName,
                null);
    }

    /**
     * 创建一个新的表配置，使用指定的表名、自定义列名和要监控的列。
     * <p>
     * 此构造函数提供完全的自定义能力，允许指定表名、更新时间戳列名、ID列名和要监控变更的特定列。
     *
     * @param cdcTableName 要监控变更的MySQL表名
     * @param updatedAtColumnName 包含记录最后更新时间戳的列名
     * @param idColumnName 包含记录唯一标识符的列名
     * @param columnsToReportOnChange 要监控变更的列名数组，如果为null或空数组则监控所有列
     * @throws IllegalArgumentException 如果提供的表名或列名无效或是MySQL保留关键字
     * @since 1.0
     */
    public PcdcTableConfig(String cdcTableName,
                           String updatedAtColumnName,
                           String idColumnName,
                           String[] columnsToReportOnChange) {
        this.cdcTableName = normalize(cdcTableName);
        this.updatedAtColumnName = normalize(updatedAtColumnName);
        this.idColumnName = normalize(idColumnName);
        String[] normalizedColumns = normalizeAll(columnsToReportOnChange);
        if (normalizedColumns.length == 0) {
            this.columnsToReportOnChange = ALL_COLUMNS;
        } else {
            this.columnsToReportOnChange = distinctJoinColumns(normalizedColumns,
                    this.idColumnName,
                    this.updatedAtColumnName);
        }
    }

    /**
     * 将多个列名数组合并为一个以逗号分隔的字符串，并添加反引号。
     * <p>
     * 此方法执行以下操作：
     * <ol>
     *   <li>合并所有提供的列名数组</li>
     *   <li>为每个列名添加MySQL反引号</li>
     *   <li>移除重复的列名</li>
     *   <li>使用逗号连接所有列名</li>
     * </ol>
     *
     * @param arr 主列名数组
     * @param args 其他列名数组
     * @return 以逗号分隔的列名字符串，每个列名都用反引号包围
     * @since 1.0
     */
    private static String distinctJoinColumns(String[] arr, String... args) {
        return Stream.of(args, arr).filter(Objects::nonNull)
                .flatMap(Arrays::stream)
                .map(s -> "`" + s + "`")
                .distinct()
                .collect(Collectors.joining(","));
    }

    /**
     * 规范化列名数组，确保所有列名符合MySQL标识符规则。
     * <p>
     * 此方法执行以下操作：
     * <ol>
     *   <li>移除每个列名的前后空白</li>
     *   <li>过滤掉空字符串</li>
     *   <li>对每个列名应用{@link #normalize(String)}方法进行规范化</li>
     * </ol>
     *
     * @param nameArr 要规范化的列名数组，可以为null
     * @return 规范化后的列名数组，如果输入为null则返回空数组
     * @throws IllegalArgumentException 如果任何列名不符合MySQL标识符规则或是保留关键字
     * @see #normalize(String)
     * @since 1.0
     */
    public static final String[] normalizeAll(String[] nameArr) {
        if (nameArr == null) {
            return new String[0];
        }
        String[] names = Stream.of(nameArr)
                .map(String::strip)
                .filter(s -> !s.isEmpty())
                .toArray(String[]::new);
        String[] t = new String[names.length];
        for (int i = 0; i < names.length; i++) {
            t[i] = normalize(names[i]);
        }
        return t;
    }

    /**
     * 规范化MySQL标识符（表名或列名），确保其符合MySQL命名规则并且不是保留关键字。
     * <p>
     * 此方法验证提供的标识符：
     * <ul>
     *   <li>以字母开头</li>
     *   <li>只包含字母、数字和下划线</li>
     *   <li>不是MySQL保留关键字</li>
     * </ul>
     *
     * @param simpleName 要规范化的MySQL标识符
     * @return 规范化后的标识符（如果有效）
     * @throws IllegalArgumentException 如果标识符格式无效，例如不以字母开头或包含非法字符
     * @throws IllegalArgumentException 如果标识符是MySQL保留关键字，可能导致SQL注入风险
     * @since 1.0
     */
    public static final String normalize(String simpleName) {
        // 1. 验证基本格式（字母开头，允许字母数字下划线）
        if (!simpleName.matches(MYSQL_IDENTIFIERS_PATTERN)) {
            throw new IllegalArgumentException("Invalid simpleName (format): " + simpleName);
        }
        // 2. 阻止传递危险MYSQL操作关键字（转为大写后检查）
        if (RISKY_MYSQL_KEYWORDS.contains(simpleName.toUpperCase())) {
            throw new IllegalArgumentException("Invalid simpleName (RISKY_MYSQL_KEYWORDS): " + simpleName);
        }
        return simpleName;
    }

    /**
     * 获取要监控变更的MySQL表名。
     *
     * @return 表名
     * @since 1.0
     */
    public String getCdcTableName() {
        return cdcTableName;
    }

    /**
     * 获取包含记录最后更新时间戳的列名。
     *
     * @return 更新时间戳列名
     * @since 1.0
     */
    public String getUpdatedAtColumnName() {
        return updatedAtColumnName;
    }

    /**
     * 获取包含记录唯一标识符的列名。
     *
     * @return ID列名
     * @since 1.0
     */
    public String getIdColumnName() {
        return idColumnName;
    }

    /**
     * 获取需要监控变更的列名列表，以逗号分隔的SQL格式。
     * <p>
     * 如果未指定特定列，则返回{@link #ALL_COLUMNS}表示监控所有列的变更。
     *
     * @return 以逗号分隔的列名列表，每个列名都用反引号包围
     * @since 1.0
     */
    public String getColumnsToReportOnChange() {
        return columnsToReportOnChange;
    }
}
